Skip to content

Fix resource wrapper method name collisions when segment equals operation ID - #61

Merged
christianhelle merged 4 commits into
mainfrom
lmstudio-spec-generation-fails
Jul 5, 2026
Merged

Fix resource wrapper method name collisions when segment equals operation ID#61
christianhelle merged 4 commits into
mainfrom
lmstudio-spec-generation-fails

Conversation

@christianhelle

@christianhelle christianhelle commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Fix a compile error in generated lmstudio.zig where resource wrapper method names collided with their containing struct names (e.g., path segment chat and operation ID chat).

Changes

  • Add needs_alias detection in ResourceWrapper when wrapper method name matches its containing struct name
  • Generate module-level alias constants (const _chat = chat;) for colliding operations
  • Prefix resource wrapper calls with alias when needs_alias is true
  • Regenerate all petstore output files
  • Add lmstudio OpenAPI spec and generated output

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added a complete OpenAPI 3.0 specification for the local LM Studio REST API, including chat and model management endpoints.
    • Documented request/response formats for chat and model workflows (load, unload, download, and download status).
  • Bug Fixes

    • Improved generated API naming and alias handling to avoid symbol collisions in overlapping routes and wrapper names, ensuring correct identifier generation in edge cases.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new OpenAPI spec for LM Studio’s local REST API and updates the Zig generator to avoid resource-wrapper naming collisions by emitting aliases. It also adds a test fixture that exercises the collision case.

Changes

LM Studio OpenAPI Specification

Layer / File(s) Summary
API metadata, security, and paths
openapi/v3.0/lmstudio.json
Defines OpenAPI metadata, server URL, bearer auth security, and chat/model-management routes.
Chat request/response schemas
openapi/v3.0/lmstudio.json
Adds chat request/response schemas, polymorphic input/output items, and chat stats/provider metadata.
Model management and download schemas
openapi/v3.0/lmstudio.json
Adds model listing, load/unload, and download job schemas plus the document closure.

API Generator Alias Handling

Layer / File(s) Summary
Wrapper collision detection and alias emission
src/generators/unified/api_generator.zig
Adds alias-tracking on resource wrappers, detects name collisions, emits underscore aliases, and rewrites generated call sites to use them.
Collision fixture and wrapper test
src/tests/resource_wrapper_tests.zig
Adds tagged-operation fixtures and a generator test that asserts aliases are skipped when top-level names collide.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the core change: fixing resource wrapper name collisions when a path segment matches an operation ID.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lmstudio-spec-generation-fails

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@christianhelle christianhelle changed the title fix: resolve resource wrapper method name collisions when segment equals operation ID Fix resource wrapper method name collisions when segment equals operation ID Jul 5, 2026
@christianhelle christianhelle self-assigned this Jul 5, 2026
@christianhelle christianhelle added the enhancement New feature or request label Jul 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/generators/unified/api_generator.zig (1)

979-986: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Confirm intentional split between rename-only vs. alias-needed collisions.

This correctly extends collision detection to ancestor/sibling declaration names, but only sets collides (renaming) — it never sets needs_alias. As noted on Line 913-920, if a wrapper's operation_id matches one of the ancestor names captured here, the resulting rename alone is insufficient; the call site still needs the module-level alias to avoid referencing the shadowed name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/generators/unified/api_generator.zig` around lines 979 - 986, The
collision detection in the wrapper handling path only marks `collides` and
misses the alias requirement for shadowing cases. Update the collision logic in
`api_generator.zig` around the wrapper name checks so that when a wrapper’s
`operation_id` or method name matches an ancestor/sibling declaration captured
in `declarations` and `wrappers`, it also sets `needs_alias` in addition to
`collides`. Keep the existing rename behavior, but ensure the code path that
detects ancestor/child struct name collisions distinguishes rename-only from
alias-needed cases.
🧹 Nitpick comments (1)
src/generators/unified/api_generator.zig (1)

852-957: 🚀 Performance & Scalability | 🔵 Trivial

Consider adding unit tests for the new alias/collision logic.

This is subtle, generator-internal logic (struct-name vs. operation-id shadowing) with no accompanying test in this file. A small test constructing a synthetic UnifiedDocument with a colliding path/operationId (mirroring the lmstudio chat case) would guard against regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/generators/unified/api_generator.zig` around lines 852 - 957, Add unit
coverage for the new alias/collision handling in generateResourceWrappers, since
this is generator-internal shadowing logic. Create a synthetic UnifiedDocument
that exercises the struct-name vs. operationId collision path (similar to the
chat case), then assert the generated output includes the expected alias symbols
and collision behavior from
UnifiedApiGenerator/resourceMethodName/resourceAliasConflicts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openapi/v3.0/lmstudio.json`:
- Around line 987-997: The load_config schema currently uses oneOf with
LlmLoadConfig and EmbeddingLoadConfig, but EmbeddingLoadConfig is a subset of
LlmLoadConfig so the variants are not mutually exclusive. Update the load_config
definition in lmstudio.json to make the two schemas distinguishable, either by
adding a discriminator or by introducing a required field in one or both
referenced schemas. Ensure the chosen fix keeps the existing load_config shape
aligned with LlmLoadConfig and EmbeddingLoadConfig while preventing ambiguous
validation.

In `@src/generators/unified/api_generator.zig`:
- Around line 923-934: The alias emission in api_generator.zig is building
identifiers from wrapper.operation_id without the same sanitization used
elsewhere, so reserved words or special characters can generate invalid Zig.
Update the alias generation inside the wrapper loop to route both the alias name
and its target through the existing identifier-sanitizing helpers used by the
generator, such as appendIdentifier or sanitizeIdentifierAlloc, consistent with
how op_id is handled elsewhere. Keep the behavior in the wrapper.operation_id
branch and the hasReturnValue(wrapper.method, wrapper.operation) result alias
generation, but ensure both emitted names are escaped/sanitized before appending
to self.buffer.
- Around line 913-920: Alias detection in the wrapper generation logic is only
checking the final segment against wrapper.method_name, but the generated call
uses wrapper.operation_id, so nested resource name collisions can still miss
needs_alias. Update the collision test in the wrapper loop to compare
wrapper.operation_id against every entry in wrapper.segments, and derive both
collides and needs_alias from that same check so ancestor/child conflicts are
handled consistently.

---

Duplicate comments:
In `@src/generators/unified/api_generator.zig`:
- Around line 979-986: The collision detection in the wrapper handling path only
marks `collides` and misses the alias requirement for shadowing cases. Update
the collision logic in `api_generator.zig` around the wrapper name checks so
that when a wrapper’s `operation_id` or method name matches an ancestor/sibling
declaration captured in `declarations` and `wrappers`, it also sets
`needs_alias` in addition to `collides`. Keep the existing rename behavior, but
ensure the code path that detects ancestor/child struct name collisions
distinguishes rename-only from alias-needed cases.

---

Nitpick comments:
In `@src/generators/unified/api_generator.zig`:
- Around line 852-957: Add unit coverage for the new alias/collision handling in
generateResourceWrappers, since this is generator-internal shadowing logic.
Create a synthetic UnifiedDocument that exercises the struct-name vs.
operationId collision path (similar to the chat case), then assert the generated
output includes the expected alias symbols and collision behavior from
UnifiedApiGenerator/resourceMethodName/resourceAliasConflicts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: acd84aac-9efb-4b04-8e8a-ad5475c894b6

📥 Commits

Reviewing files that changed from the base of the PR and between b191922 and f5f63b4.

⛔ Files ignored due to path filters (7)
  • generated/generated_v2.zig is excluded by !**/generated/**
  • generated/generated_v2_yaml.zig is excluded by !**/generated/**
  • generated/generated_v3.zig is excluded by !**/generated/**
  • generated/generated_v32.zig is excluded by !**/generated/**
  • generated/generated_v3_yaml.zig is excluded by !**/generated/**
  • generated/lmstudio.zig is excluded by !**/generated/**
  • generated/main.zig is excluded by !**/generated/**
📒 Files selected for processing (2)
  • openapi/v3.0/lmstudio.json
  • src/generators/unified/api_generator.zig

Comment on lines +987 to +997
"load_config": {
"description": "The final configuration applied to the loaded model. Included only when echo_load_config is true",
"oneOf": [
{
"$ref": "#/components/schemas/LlmLoadConfig"
},
{
"$ref": "#/components/schemas/EmbeddingLoadConfig"
}
]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
import json, re, pathlib, sys

path = pathlib.Path('openapi/v3.0/lmstudio.json')
data = json.loads(path.read_text())

schemas = data['components']['schemas']
for name in ['LlmLoadConfig', 'EmbeddingLoadConfig']:
    print(f'### {name}')
    schema = schemas[name]
    print(json.dumps(schema, indent=2)[:4000])
    print()

load_cfg = schemas['LoadModelResponse']['properties']['load_config']
print('### LoadModelResponse.load_config')
print(json.dumps(load_cfg, indent=2))
PY

Repository: christianhelle/openapi2zig

Length of output: 1504


🏁 Script executed:

python3 - <<'PY'
import json, pathlib

path = pathlib.Path('openapi/v3.0/lmstudio.json')
data = json.loads(path.read_text())

schemas = data['components']['schemas']
for name in ['LlmLoadConfig', 'EmbeddingLoadConfig']:
    print(f'### {name}')
    print(json.dumps(schemas[name], indent=2))
    print()

print('### LoadModelResponse.load_config')
print(json.dumps(schemas['LoadModelResponse']['properties']['load_config'], indent=2))
PY

Repository: christianhelle/openapi2zig

Length of output: 1504


load_config needs a mutually exclusive schema. EmbeddingLoadConfig is a subset of LlmLoadConfig (context_length only), so a payload like {"context_length": 4096} matches both and breaks oneOf validation. Add a discriminator or another required field to separate the variants.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openapi/v3.0/lmstudio.json` around lines 987 - 997, The load_config schema
currently uses oneOf with LlmLoadConfig and EmbeddingLoadConfig, but
EmbeddingLoadConfig is a subset of LlmLoadConfig so the variants are not
mutually exclusive. Update the load_config definition in lmstudio.json to make
the two schemas distinguishable, either by adding a discriminator or by
introducing a required field in one or both referenced schemas. Ensure the
chosen fix keeps the existing load_config shape aligned with LlmLoadConfig and
EmbeddingLoadConfig while preventing ambiguous validation.

Comment thread src/generators/unified/api_generator.zig
Comment on lines +923 to +934
for (wrappers.items) |wrapper| {
if (wrapper.needs_alias) {
const alias_line = try std.fmt.allocPrint(self.allocator, "const _{s} = {s};\n", .{ wrapper.operation_id, wrapper.operation_id });
defer self.allocator.free(alias_line);
try self.buffer.appendSlice(self.allocator, alias_line);
if (self.hasReturnValue(wrapper.method, wrapper.operation)) {
const result_line = try std.fmt.allocPrint(self.allocator, "const _{s}Result = {s}Result;\n", .{ wrapper.operation_id, wrapper.operation_id });
defer self.allocator.free(result_line);
try self.buffer.appendSlice(self.allocator, result_line);
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Alias constant names/targets aren't sanitized/escaped like other emitted identifiers.

alias_line/result_line are built by directly interpolating wrapper.operation_id into the source text, bypassing appendIdentifier/sanitizeIdentifierAlloc, which is used everywhere else in this file for spec-derived identifiers (e.g. Line 1350: try self.appendIdentifier(op_id)). If operation_id is a Zig reserved word or contains characters requiring @"..." escaping, this produces invalid Zig source.

🐛 Proposed fix
-        for (wrappers.items) |wrapper| {
-            if (wrapper.needs_alias) {
-                const alias_line = try std.fmt.allocPrint(self.allocator, "const _{s} = {s};\n", .{ wrapper.operation_id, wrapper.operation_id });
-                defer self.allocator.free(alias_line);
-                try self.buffer.appendSlice(self.allocator, alias_line);
-                if (self.hasReturnValue(wrapper.method, wrapper.operation)) {
-                    const result_line = try std.fmt.allocPrint(self.allocator, "const _{s}Result = {s}Result;\n", .{ wrapper.operation_id, wrapper.operation_id });
-                    defer self.allocator.free(result_line);
-                    try self.buffer.appendSlice(self.allocator, result_line);
-                }
-            }
-        }
+        for (wrappers.items) |wrapper| {
+            if (wrapper.needs_alias) {
+                try self.buffer.appendSlice(self.allocator, "const _");
+                try self.appendIdentifier(wrapper.operation_id);
+                try self.buffer.appendSlice(self.allocator, " = ");
+                try self.appendIdentifier(wrapper.operation_id);
+                try self.buffer.appendSlice(self.allocator, ";\n");
+                if (self.hasReturnValue(wrapper.method, wrapper.operation)) {
+                    const operation_result_name = try std.fmt.allocPrint(self.allocator, "{s}Result", .{wrapper.operation_id});
+                    defer self.allocator.free(operation_result_name);
+                    try self.buffer.appendSlice(self.allocator, "const _");
+                    try self.appendIdentifier(operation_result_name);
+                    try self.buffer.appendSlice(self.allocator, " = ");
+                    try self.appendIdentifier(operation_result_name);
+                    try self.buffer.appendSlice(self.allocator, ";\n");
+                }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/generators/unified/api_generator.zig` around lines 923 - 934, The alias
emission in api_generator.zig is building identifiers from wrapper.operation_id
without the same sanitization used elsewhere, so reserved words or special
characters can generate invalid Zig. Update the alias generation inside the
wrapper loop to route both the alias name and its target through the existing
identifier-sanitizing helpers used by the generator, such as appendIdentifier or
sanitizeIdentifierAlloc, consistent with how op_id is handled elsewhere. Keep
the behavior in the wrapper.operation_id branch and the
hasReturnValue(wrapper.method, wrapper.operation) result alias generation, but
ensure both emitted names are escaped/sanitized before appending to self.buffer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/tests/resource_wrapper_tests.zig (2)

147-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering .paths mode too.

The fixture's path (/api/v1/chat) would also trigger the same last-segment collision under .paths mode, so including it would close a coverage gap for this exact regression.

♻️ Suggested addition
-    const modes = [_]cli.ResourceWrapperMode{ .tags, .hybrid };
+    const modes = [_]cli.ResourceWrapperMode{ .tags, .paths, .hybrid };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tests/resource_wrapper_tests.zig` at line 147, The ResourceWrapper mode
coverage in the test fixture currently skips .paths, leaving the last-segment
collision regression untested. Update the modes array in
resource_wrapper_tests.zig to include .paths alongside .tags and .hybrid, and
keep the existing fixture path (/api/v1/chat) so the same collision case is
exercised through ResourceWrapper behavior.

145-168: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

New test uses std.testing.allocator instead of the required test allocator pattern.

As per coding guidelines, **/tests/**/*.zig files should "Use test_utils.createTestAllocator() in all tests for leak detection" with the pattern var gpa = test_utils.createTestAllocator(); const allocator = gpa.allocator(); defer document.deinit(allocator);. This new test (and the file overall) uses std.testing.allocator directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tests/resource_wrapper_tests.zig` around lines 145 - 168, The new test in
resource_wrapper_tests.zig is using std.testing.allocator directly instead of
the required leak-detecting test allocator pattern. Update this test to use
test_utils.createTestAllocator() and obtain the allocator from it, following the
same setup/teardown style used in other tests so allocations are tracked
properly. Keep the existing document.deinit(allocator), generator lifecycle, and
assertions unchanged, but make sure the test uses the shared test_utils
allocator pattern consistently across this file.

Source: Coding guidelines

src/generators/unified/api_generator.zig (1)

1246-1271: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bare catch return true; without |err|/context.

The new operationDeclaresTopLevelName swallows allocation failures via catch return true; at Lines 1251, 1256, and 1264, without capturing |err| or a context message. As per coding guidelines, **/*.zig files should "Use Zig error sets and catch |err| pattern for error handling with meaningful context messages." (Mirrors an existing pattern at Line 1238, but this PR introduces new occurrences.)

♻️ Example fix
-        const raw_name = std.fmt.allocPrint(self.allocator, "{s}Raw", .{operation_id}) catch return true;
+        const raw_name = std.fmt.allocPrint(self.allocator, "{s}Raw", .{operation_id}) catch |err| {
+            std.log.warn("failed to allocate alias-conflict name for '{s}': {s}", .{ operation_id, `@errorName`(err) });
+            return true;
+        };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/generators/unified/api_generator.zig` around lines 1246 - 1271, The new
operationDeclaresTopLevelName helper is swallowing allocation failures with bare
catch return true, which bypasses the required Zig error handling pattern.
Update each allocPrint call in operationDeclaresTopLevelName to capture the
error with |err| and add a meaningful context message before deciding how to
proceed, matching the existing style used elsewhere in UnifiedApiGenerator. Keep
the fallback behavior explicit, but do not leave the allocation failures
unreported or context-free.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/generators/unified/api_generator.zig`:
- Around line 1246-1271: The new operationDeclaresTopLevelName helper is
swallowing allocation failures with bare catch return true, which bypasses the
required Zig error handling pattern. Update each allocPrint call in
operationDeclaresTopLevelName to capture the error with |err| and add a
meaningful context message before deciding how to proceed, matching the existing
style used elsewhere in UnifiedApiGenerator. Keep the fallback behavior
explicit, but do not leave the allocation failures unreported or context-free.

In `@src/tests/resource_wrapper_tests.zig`:
- Line 147: The ResourceWrapper mode coverage in the test fixture currently
skips .paths, leaving the last-segment collision regression untested. Update the
modes array in resource_wrapper_tests.zig to include .paths alongside .tags and
.hybrid, and keep the existing fixture path (/api/v1/chat) so the same collision
case is exercised through ResourceWrapper behavior.
- Around line 145-168: The new test in resource_wrapper_tests.zig is using
std.testing.allocator directly instead of the required leak-detecting test
allocator pattern. Update this test to use test_utils.createTestAllocator() and
obtain the allocator from it, following the same setup/teardown style used in
other tests so allocations are tracked properly. Keep the existing
document.deinit(allocator), generator lifecycle, and assertions unchanged, but
make sure the test uses the shared test_utils allocator pattern consistently
across this file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 666b07ad-8cfb-4231-b01b-109f193d2449

📥 Commits

Reviewing files that changed from the base of the PR and between f5f63b4 and dd5bf78.

📒 Files selected for processing (2)
  • src/generators/unified/api_generator.zig
  • src/tests/resource_wrapper_tests.zig

@christianhelle
christianhelle merged commit 21d0de3 into main Jul 5, 2026
17 checks passed
@christianhelle
christianhelle deleted the lmstudio-spec-generation-fails branch July 5, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant